package data

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/go-kratos/kratos/v2/log"
	userBiz "go-skeleton/app/v1/service/internal/biz/user"
	"go-skeleton/app/v1/service/internal/data/ent"
	"go-skeleton/app/v1/service/internal/data/ent/user"
	"go-skeleton/pkg/util"
	"go-skeleton/pkg/util/pagination"
	"math/rand"
	"strconv"
	"time"
)

type UserRepo struct {
	data *Data
	log  *log.Helper
}

// NewUserRepo .
func NewUserRepo(data *Data, logger log.Logger) userBiz.UserRepo {
	return &UserRepo{
		data: data,
		log:  log.NewHelper(log.With(logger, "module", "data/user")),
	}
}

var userCacheKey = func(key string) string {
	return "user_cache_key_" + key
}

var sendCodeKey = func(phone string) string {
	return "send_code_" + phone
}

func (r *UserRepo) Save(ctx context.Context, g *ent.User) (*ent.User, error) {
	ph, err := util.HashPassword(g.Password)
	if err != nil {
		return nil, err
	}

	phoneEncrypt := util.PhoneEncrypt(g.Phone)
	target, err := r.data.db.User.Create().SetNickName(g.NickName).SetAvatar(g.Avatar).SetPassword(ph).SetPhone(phoneEncrypt).Save(ctx)
	if err != nil {
		return nil, err
	}

	r.setUserCache(ctx, target, userCacheKey(strconv.FormatInt(g.ID, 10)))
	r.setUserCache(ctx, target, userCacheKey(phoneEncrypt))
	return target, nil
}

func (r *UserRepo) Update(ctx context.Context, g *ent.User) (*ent.User, error) {
	ph, err := util.HashPassword(g.Password)
	if err != nil {
		return nil, err
	}

	target, err := r.data.db.User.UpdateOneID(g.ID).SetNickName(g.NickName).SetAvatar(g.Avatar).SetPassword(ph).Save(ctx)
	if err != nil {
		return nil, err
	}

	r.setUserCache(ctx, target, userCacheKey(strconv.FormatInt(g.ID, 10)))
	r.setUserCache(ctx, target, target.Phone)
	return target, nil
}

func (r *UserRepo) FindByID(ctx context.Context, id int64) (*ent.User, error) {
	cacheKey := userCacheKey(strconv.FormatInt(id, 10))
	target, err := r.getUserFromCache(ctx, cacheKey)

	if err != nil {
		target, err = r.data.db.User.Get(ctx, id)
		if err != nil {
			return nil, userBiz.ErrUserNotFound
		}

		// set cache
		r.setUserCache(ctx, target, cacheKey)
	}
	target.Phone = util.PhoneDecrypt(target.Phone)

	return target, nil
}

func (r *UserRepo) FindByPhone(ctx context.Context, phone string) (*ent.User, error) {
	cacheKey := userCacheKey(util.PhoneEncrypt(phone))
	target, err := r.getUserFromCache(ctx, cacheKey)

	if err != nil {
		target, err = r.data.db.User.Query().Where(user.PhoneEQ(util.PhoneEncrypt(phone))).Only(ctx)
		if err != nil {
			return nil, userBiz.ErrUserNotFound
		}

		// set cache
		r.setUserCache(ctx, target, cacheKey)
	}
	target.Phone = util.PhoneDecrypt(target.Phone)

	return target, nil
}

func (r *UserRepo) ListUser(ctx context.Context, pageNum, pageSize int64) ([]*ent.User, error) {
	target, err := r.data.db.User.Query().
		Offset(int(pagination.GetPageOffset(pageNum, pageSize))).
		Limit(int(pageSize)).All(ctx)
	if err != nil {
		return nil, err
	}
	return target, nil
}

func (r *UserRepo) getUserFromCache(ctx context.Context, key string) (*ent.User, error) {
	result, err := r.data.rdb.Get(ctx, key).Result()
	if err != nil {
		return nil, err
	}
	var cacheUser = &ent.User{}
	err = json.Unmarshal([]byte(result), cacheUser)
	if err != nil {
		return nil, err
	}
	return cacheUser, nil
}

func (r *UserRepo) setUserCache(ctx context.Context, user *ent.User, key string) {
	marshal, err := json.Marshal(user)
	if err != nil {
		r.log.Errorf("fail to set user cache:json.Marshal(%v) error(%v)", user, err)
	}
	err = r.data.rdb.Set(ctx, key, string(marshal), time.Minute*30).Err()
	if err != nil {
		r.log.Errorf("fail to set user cache:redis.Set(%v) error(%v)", user, err)
	}
}

func (r *UserRepo) SendCode(ctx context.Context, phone string) error {
	cacheKey := sendCodeKey(phone)
	code, err := r.data.rdb.Get(ctx, cacheKey).Result()
	if err != nil || code == "" {
		code = fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
		err = r.data.rdb.Set(ctx, cacheKey, code, time.Minute).Err()
		if err != nil {
			r.log.Errorf("fail to set code cache:redis.Set(%v) error(%v)", code, err)
		}

		return nil
	}
	return userBiz.ErrVerifyCodeFrequent
}

func (r *UserRepo) VerifyCode(ctx context.Context, phone, code string) (bool, error) {
	cacheKey := sendCodeKey(phone)
	verifyCode, err := r.data.rdb.Get(ctx, cacheKey).Result()
	r.data.rdb.Del(ctx, cacheKey)
	if err != nil || verifyCode == "" || verifyCode != code {
		return false, userBiz.ErrVerifyCodeErr
	}
	return true, nil
}

func (r *UserRepo) VerifyPassword(ctx context.Context, u *ent.User) (bool, error) {
	po, err := r.FindByPhone(ctx, u.Phone)
	if err != nil {
		return false, err
	}
	return util.CheckPasswordHash(u.Password, po.Password), nil
}
